#include <chrono>
#include <fcntl.h>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <ostream>
#include <signal.h>  // NOLINTNEXTLINE(modernize-deprecated-headers)
#include <vector>

#include "ecmascript/base/string_helper.h"
#include "ecmascript/ecma_string.h"
#include "ecmascript/ecma_vm.h"
#include "ecmascript/js_runtime_options.h"
#include "ecmascript/log.h"
#include "ecmascript/mem/mem_controller.h"
#include "ecmascript/mem/clock_scope.h"
#include "ecmascript/napi/include/jsnapi.h"

#include "frontend/frontend.h"
#include "cov/cov.h"


#define REPRL_CRFD 100  // Control read file decriptor
#define REPRL_CWFD 101  // Control write file decriptor
#define REPRL_DRFD 102  // Data read file decriptor
#define REPRL_DWFD 103  // Data write file decriptor
bool fuzzilli_reprl = true;
char tmp[12] = "print(1337)";

namespace panda::ecmascript {

void BlockSignals() {
    sigset_t set;
    if (sigemptyset(&set) == -1) {
        LOG_ECMA(ERROR) << "sigemptyset failed";
        return;
    }
}

std::string GetHelper() {
    std::string str;
    str.append(COMMON_HELP_HEAD_MSG);
    str.append(HELP_OPTION_MSG);
    return str;
}

panda::Local<panda::JSValueRef> Fuzzilli(panda::JsiRuntimeCallInfo* runtimeCallInfo) {
    EcmaVM* vm = runtimeCallInfo->GetVM();
    int32_t argc = runtimeCallInfo->GetArgsNumber();
    if (argc < 2) {
        return panda::JSValueRef::Undefined(vm);     
    }
    Local<JSValueRef> arg0 = runtimeCallInfo->GetCallArgRef(0);
    if (!arg0->IsString(vm)) {
        return panda::JSValueRef::Undefined(vm);
    }
    Local<JSValueRef> arg1 = runtimeCallInfo->GetCallArgRef(1);

    std::string operation = arg0->ToString(vm)->ToString(vm);
    if (strcmp(operation.c_str(), "FUZZILLI_CRASH") == 0) {
        if (!arg1->IsNumber()) {
            return panda::JSValueRef::Undefined(vm);
        }
        auto cmd = arg1->Int32Value(vm);
        switch(cmd) {
            case 0: {
                ASSERT(false);
                break;
            }
                
            case 1: {
                char* ptr = reinterpret_cast<char*>(0x414141414141ull);
                for (int i = 0; i < 1024; i++) {
                    *ptr = 'A';
                    ptr += 1 * 0x4000;
                }
                break;
            }
                
            case 2: {
                // Use-after-free, likely only crashes in ASan builds.
                auto* vec = new std::vector<int>(4);
                delete vec;
                vec->at(0);
                break;
            }

            case 3: {
                // Out-of-bounds access (1), likely only crashes in ASan or
                // "hardened"/"safe" libc++ builds.
                std::vector<int> vec(5);
                vec[5];
                break;
            }

            case 4: {
                // Out-of-bounds access (2), likely only crashes in ASan builds.
                std::vector<int> vec(6);
                memset(vec.data(), 42, 0x100);
                break;
            }
                
            default:
                break;
        }
    } else if (strcmp(operation.c_str(), "FUZZILLI_PRINT") == 0) {
        static FILE* fzliout = fdopen(REPRL_DWFD, "w");
        if (!fzliout) {
            fprintf(
                stderr,
                "Fuzzer output channel not available, printing to stdout instead\n");
                fzliout = stdout;
        }
        if (!arg1->IsString(vm)) {
            return panda::JSValueRef::Undefined(vm);
        }
        std::string data = arg1->ToString(vm)->ToString(vm);
        fprintf(fzliout, "%s\n", data.c_str());
        fflush(fzliout);
    }
    return panda::JSValueRef::Undefined(vm);
}

int Main(const int argc, const char **argv) {
    BlockSignals();

    std::ofstream log("log.txt", std::ios::app);
    JSRuntimeOptions runtimeOptions;
    bool retOpt = runtimeOptions.ParseCommand(argc, argv);
    if (!retOpt) {
        std::cerr << GetHelper();
        return 1;
    }

    char helo[] = "HELO";
    if (write(REPRL_CWFD, helo, 4) != 4 || read(REPRL_CRFD, helo, 4) != 4) {
        fuzzilli_reprl = false;
    }
    if (memcmp(helo, "HELO", 4) != 0) {
        LOG_ECMA(ERROR) << "REPRL: Invalid response from parent";
        return -1;
    }

    size_t totalIndex = 0;
    size_t compileFilesfailedIndex = 0;
    size_t parseFilesfailedIndex = 0;
    size_t generateAbcFilesfailedIndex = 0;

    do {
        size_t script_size;
        char* buffer;
        if (fuzzilli_reprl) {
            unsigned action = 0;
            ssize_t nread = read(REPRL_CRFD, &action, 4);
            if (nread != 4 || action != 'cexe') {
                LOG_ECMA(ERROR) << "REPRL: Unknown action: " << action;
                exit(1);
            }

            if (read(REPRL_CRFD, &script_size, 8) != 8) {
                LOG_ECMA(ERROR) << "REPRL: Size error";
                exit(1);
            }
            buffer = new char[script_size + 1];
            char* ptr = buffer;
            size_t remaining = script_size;
            while (remaining > 0) {
                ssize_t rv = read(REPRL_DRFD, ptr, remaining);
                if (rv < 0) {
                    LOG_ECMA(ERROR) << "REPRL: Recv error";
                    exit(1);
                }
                remaining -= rv;
                ptr += rv;
            }
            buffer[script_size] = 0;
        } else {
            const char* fname = getenv("JS");
            std::ifstream in(fname, std::ifstream::ate | std::ifstream::binary);
            script_size = in.tellg();
            in.seekg(0, std::ios::beg);
            buffer = new char[script_size + 1];
            in.read(buffer, script_size);
        }

        std::string entry = "_GLOBAL::func_main_0";
        char* data = NULL;
        int32_t size = 0;
        std::string parseResult = Parse(0, NULL, buffer, script_size, &data, size);
        if (parseResult != "0") {
            std::string directory = "frontend_crashes";
            std::filesystem::path directoryPath = directory;
            if (!std::filesystem::exists(directoryPath)) {
                std::filesystem::create_directory(directoryPath);
            }

            totalIndex++;
            std::string totalSampleNumber = std::to_string(totalIndex);

            std::string failedType = "";
            std::string failedTypeNumber = "";
            if (parseResult == "CompileFilesfailed") {
                failedType = "CompileFilesfailed";
                compileFilesfailedIndex++;
                failedTypeNumber = std::to_string(compileFilesfailedIndex);
            } else if(parseResult == "ParseFilesfailed") {
                failedType = "ParseFilesfailed";
                parseFilesfailedIndex++;
                parseResult = std::to_string(parseFilesfailedIndex);
            } else if(parseResult == "GenerateAbcFilesfailed") {
                failedType = "GenerateAbcFilesfailed";
                generateAbcFilesfailedIndex++;
                failedTypeNumber = std::to_string(generateAbcFilesfailedIndex);
            }

            std::string fileName = failedType + failedTypeNumber + "_" +"totalSample" + totalSampleNumber + ".js";
            std::string fileDirectory = directory + "/" + fileName;
            std::ofstream outputFile(fileDirectory);
            if (!outputFile) {
                std::cerr << "Error: Failed to open output file!" << std::endl;
                return 1;
            }

            outputFile << buffer;
            outputFile.close();
        }

        delete []buffer;

        int parseStatus = 0;
        if (parseResult != "0") {
            parseStatus = 1;
        }

        bool result = parseStatus;
        if (!parseStatus) {
            EcmaVM *vm = JSNApi::CreateEcmaVM(runtimeOptions);
            if (vm == nullptr) {
                std::cerr << "Cannot Create vm" << std::endl;
                return -1;
            }
            auto globalObj = JSNApi::GetGlobalObject(vm);
            globalObj->Set(vm, panda::StringRef::NewFromUtf8(vm, "fuzzilli"),
                panda::FunctionRef::New(vm, Fuzzilli));
            bool isMergeAbc = runtimeOptions.GetMergeAbc();
            JSNApi::SetBundle(vm, !isMergeAbc);
            {
                LocalScope scope(vm);
                auto exec_success = JSNApi::Execute(vm, (const uint8_t *)data, size, entry);
                if (JSNApi::HasPendingException(vm)) {
                    JSNApi::PrintExceptionInfo(vm);
                    JSNApi::GetAndClearUncaughtException(vm);
                    exec_success = false;
                }
                JSNApi::TriggerGC(vm, JSNApi::TRIGGER_GC_TYPE::FULL_GC);
                result = result | (!exec_success);
            }
            JSNApi::DestroyJSVM(vm);
        }

        if (fuzzilli_reprl) {
            int res = result ? 1 : 0;
            int status = res << 8;

            // In REPRL mode, stdout and stderr can be regular files, so they need
            // to be flushed after every execution
            fflush(stdout);
            fflush(stderr);
            if (write(REPRL_CWFD, &status, 4) != 4) {
                LOG_ECMA(ERROR) << "REPRL Failed";
            };
            sanitizer_cov_reset_edgeguards();
        }
    } while (fuzzilli_reprl);
        
    
    return 0;
}
}  // namespace panda::ecmascript

int main(int argc, const char **argv) {
    return panda::ecmascript::Main(argc, argv);
}